package cn.demoncat.util.excel;

import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelXorHtmlUtil;
import cn.afterturn.easypoi.excel.entity.TemplateExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.demoncat.util.excel.entity.FieldFormat;
import cn.demoncat.util.excel.entity.FieldName;
import cn.demoncat.util.excel.entity.HeadField;
import cn.demoncat.util.exception.BizRuntimeException;
import cn.demoncat.util.exception.IoRuntimeException;
import cn.demoncat.util.exception.ParamRuntimeException;
import cn.demoncat.util.exception.ReflexRuntimeException;
import cn.demoncat.util.io.CloseUtil;
import cn.demoncat.util.io.FileNameUtil;
import cn.demoncat.util.io.IoUtil;
import cn.demoncat.util.lang.*;
import cn.demoncat.util.lang.constant.DateConstant;
import cn.demoncat.util.lang.constant.StringConstant;
import cn.demoncat.util.web.ServletUtil;
import ognl.Ognl;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFFooter;
import org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException;
import org.apache.poi.poifs.filesystem.OfficeXmlFileException;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.function.Consumer;

/**
 * POI-Excel工具
 * 
 * <pre>
 * # 导出
 * 
 * 1、列宽：默认为标题字数+10
 * 2、日期类型：默认输出yyyy-MM-dd HH:mm:ss格式的文本
 * 3、数字类型：未设置FieldFormat = 输出toString()文本；设置FieldFormat = 转换为double，由POI格式化并输出为数字
 * 4、其它类型：输出toString()文本
 * 5、空数据：输出''
 * 6、如果需要对数据进行转换，可以在传入数据之前整理数据，或者定义辅助字段并在get方法中处理原字段
 * 7、可以设置是否打印页脚
 * 8、Excel的一个sheet允许的最大行数是65536，同时数据量过大时会内存溢出，因此建议在导出时限制导出的数据量
 *
 * 模板示例：https://gitee.com/lemur/easypoi-test/tree/master/src/test/resources/doc
 *
 * # 导入
 * 
 * 1、读取单元格，返回List<行:<单元格>>
 * 2、统一读取数据为字符串，调用者可以先判断字符串是否为空，然后再进行类型转换
 * 3、可以设置读取的起始行、结束行、起始列、结束列、工作表
 *
 * # 版本
 * HSSFWorkbook：操作Excel2003的版本，扩展名是.xls，限制65536条
 * XSSFWorkbook：操作Excel2007的版本，扩展名是.xlsx，可能OutOfMemoryError
 * SXSSFWorkbook：XSSFWorkbook的超级版本，用于导出大量数据
 * 对于不同版本的EXCEL文档要使用不同的工具类，否则报错OfficeXmlFileException
 * </pre>
 * 
 * @author 延晓磊
 *
 * @since 2016年8月19日
 */
public class ExcelUtil {

	// 文件扩展名：Excel2007
	public static final String EXCEL_TAG = ".xlsx";
	// 字体
	public static final String FONT_NAME = "宋体";
	// 文件类型阀值：XSSFWorkbook < n < SXSSFWorkbook
	private static final int EXCEL_LEVEL = 10000;

	// 异常消息
	private static final String EXP = "导出Excel失败：";
	private static final String EXP_FIELD = "导出Excel失败：表头field为空";
	private static final String EXP_DATA = "数据获取失败：";


	/**
	 * 导出Excel - HTML
	 *
	 * @param html			HTML内容
	 * @param fileName      文件名，不需要后缀
	 * @param response
	 *
	 * @author 延晓磊
	 * @since 2021年12月31日
	 */
	public static void exportByHtml(String html, String fileName, HttpServletResponse response){
		exportByHtml(html, ServletUtil.getDownloadOut(response, fileName + EXCEL_TAG));
	}

	/**
	 * 导出Excel - HTML
	 *
	 * @param html			HTML内容
	 * @param handler	处理器
	 * @param fileName      文件名，不需要后缀
	 * @param response
	 *
	 * @author 延晓磊
	 * @since 2021年12月31日
	 */
	public static void exportByHtml(String html, Consumer<Workbook> handler, String fileName, HttpServletResponse response){
		exportByHtml(html, handler, ServletUtil.getDownloadOut(response, fileName + EXCEL_TAG));
	}

	/**
	 * 导出Excel - HTML
	 *
	 * @param html	HTML内容
	 * @param out	输出文件：*.xlsx
	 *
	 * @author 延晓磊
	 * @since 2021年12月31日
	 */
	public static void exportByHtml(String html, OutputStream out){
		exportByHtml(html, null, out);
	}

	/**
	 * 导出Excel - HTML
	 *
	 * <pre>
	 * 1、结构：<table sheetName="Sheet1"><tr><td><td/><tr/></table>
	 * 2、样式： cn.afterturn.easypoi.excel.html.entity.HtmlCssConstant
	 * 		合并：colspan,rowspan
	 * 		冻结：<tr freezeRow="true">，<td freezeCol="true">
	 * 		样式：style="width: 20px; height: 24px; text-align: center; vertical-align: top; color: red; background: gray;"
	 * 		边框：style="border: #f00; text-align: center"
	 * 		字体： style="font: bold 22pt 'Microsoft YaHei';"
	 * </>
	 * @param html	HTML内容
	 * @param handler	处理器
	 * @param out	输出文件：*.xlsx
	 *
	 * @author 延晓磊
	 * @since 2021年12月31日
	 */
	public static void exportByHtml(String html, Consumer<Workbook> handler, OutputStream out){
		Workbook workbook = ExcelXorHtmlUtil.htmlToExcel(html, ExcelType.XSSF);
		try {
			// 自定义处理
			if (handler != null) {
				handler.accept(workbook);
			}
			// 导出
			workbook.write(out);
		} catch (IOException e) {
			throw new IoRuntimeException("导出Excel失败：" + e.getMessage(), e);
		}finally {
			CloseUtil.flush(out);
			CloseUtil.close(out);
			CloseUtil.close(workbook);
		}
	}

	/**
	 * 导出Excel - 模板
	 *
	 * @param templateUrl   模板路径：绝对路径、相对路径（基于ClassPath，如 *resources/ptl/1.xlsx = ptl/1.xlsx）、网络路径（http开头）
	 * @param data          数据
	 * @param fileName      文件名，不需要后缀
	 * @param response
	 *
	 * @author 延晓磊
	 * @since 2021年12月31日
	 */
	public static void exportByPtl(String templateUrl, Map<String,Object> data, String fileName, HttpServletResponse response){
		exportByPtl(templateUrl, data, ServletUtil.getDownloadOut(response, fileName + FileNameUtil.getSuffix(templateUrl)));
	}

	/**
	 * 导出Excel - 模板
	 *
	 * @param templateUrl   模板路径：绝对路径、相对路径（基于ClassPath，如 *resources/ptl/1.xlsx = ptl/1.xlsx）、网络路径（http开头）
	 * @param data          数据
	 * @param handler		处理器
	 * @param fileName      文件名，不需要后缀
	 * @param response
	 *
	 * @author 延晓磊
	 * @since 2021年12月31日
	 */
	public static void exportByPtl(String templateUrl, Map<String,Object> data, Consumer<Workbook> handler, String fileName, HttpServletResponse response){
		exportByPtl(templateUrl, data, handler, ServletUtil.getDownloadOut(response, fileName + FileNameUtil.getSuffix(templateUrl)));
	}

	/**
	 * 导出Excel - 模板
	 *
	 * @param templateUrl   模板路径：绝对路径、相对路径（基于ClassPath，如 *resources/ptl/1.xlsx = ptl/1.xlsx）、网络路径（http开头）
	 * @param data          数据
	 * @param out           输出文件：*.xlsx/*.xls，后缀名须和模板一致，否则可能因格式错误打开失败
	 *
	 * @author 延晓磊
	 * @since 2021年12月31日
	 */
	public static void exportByPtl(String templateUrl, Map<String,Object> data, OutputStream out){
		exportByPtl(templateUrl, data, null, out);
	}

	/**
	 * 导出Excel - 模板
	 *
	 * @param templateUrl   模板路径：绝对路径、相对路径（基于ClassPath，如 *resources/ptl/1.xlsx = ptl/1.xlsx）、网络路径（http开头）
	 * @param data          数据
	 * @param handler		处理器
	 * @param out           输出文件：*.xlsx/*.xls，后缀名须和模板一致，否则可能因格式错误打开失败
	 *
	 * @author 延晓磊
	 * @since 2021年12月31日
	 */
	public static void exportByPtl(String templateUrl, Map<String,Object> data, Consumer<Workbook> handler, OutputStream out){
		// 加载模板
		TemplateExportParams template = new TemplateExportParams(templateUrl, true);
		// 支持 #fe/v_fe横向遍历
		template.setColForEach(true);
		// 创建文档
		Workbook workbook = ExcelExportUtil.exportExcel(template, data);
		try {
			// 自定义处理
			if (handler != null) {
				handler.accept(workbook);
			}
			// 导出
			workbook.write(out);
		} catch (IOException e) {
			throw new IoRuntimeException("导出Excel失败：" + e.getMessage(), e);
		} finally {
			CloseUtil.flush(out);
			CloseUtil.close(out);
			CloseUtil.close(workbook);
		}
	}

	/**
	 * 导出Excel到OutputStream
	 * 
	 * @param title
	 *            表格标题，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param out
	 * 			  输出流：*.xlsx
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String title, List<HeadField> heads, List<T> datas, OutputStream out) {
		export(title, heads, datas, out, false);
	}

	/**
	 * 导出Excel到OutputStream
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param out
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String title, List<HeadField> heads, List<T> datas, OutputStream out, boolean printFooter) {
		export(title, heads, datas, out, printFooter, true);
	}

	/**
	 * 导出Excel到HttpServletResponse
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 * 				输出流：*.xlsx
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String title, List<HeadField> heads, List<T> datas, HttpServletResponse response) {
		export(title, heads, datas, response, false);
	}

	/**
	 * 导出Excel到HttpServletResponse
	 *
	 * @param fileName
	 *            文件名
	 * @param title
	 *            表格标题/文件名
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 * 				输出流：*.xlsx
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String fileName, String title, List<HeadField> heads, List<T> datas, HttpServletResponse response) {
		export(fileName, title, heads, datas, response, false);
	}

	/**
	 * 导出Excel到HttpServletResponse
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 * 				输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String title, List<HeadField> heads, List<T> datas, HttpServletResponse response, boolean printFooter) {
		export(title, heads, datas, response, printFooter, true);
	}

	/**
	 * 导出Excel到HttpServletResponse
	 *
	 * @param fileName
	 *            文件名
	 * @param title
	 *            表格标题/文件名
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 * 				输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 *
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String fileName, String title, List<HeadField> heads, List<T> datas, HttpServletResponse response, boolean printFooter) {
		export(fileName, title, heads, datas, response, printFooter, true);
	}

	/**
	 * 导出带（列）合并单元格的Excel到OutputStream
	 * 
	 * @param title
	 *            表格标题，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param out
	 * 			  输出流：*.xlsx
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String title, List<HeadField> heads, List<T> datas, OutputStream out) {
		exportMerge(title, heads, datas, out, false);
	}

	/**
	 * 导出带（列）合并单元格的Excel到OutputStream
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param out
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String title, List<HeadField> heads, List<T> datas, OutputStream out, boolean printFooter) {
		exportMerge(title, heads, datas, out, printFooter, true);
	}

	/**
	 * 导出带（列）合并单元格的Excel到HttpServletResponse
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 *  		  输出流：*.xlsx
	 *
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String title, List<HeadField> heads, List<T> datas, HttpServletResponse response) {
		exportMerge(title, heads, datas, response, false);
	}

	/**
	 * 导出带（列）合并单元格的Excel到HttpServletResponse
	 *
	 * @param fileName	文件名
	 * @param title
	 *            表格标题/文件名
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 *  		  输出流：*.xlsx
	 *
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String fileName, String title, List<HeadField> heads, List<T> datas, HttpServletResponse response) {
		exportMerge(fileName, title, heads, datas, response, false);
	}

	/**
	 * 导出带（列）合并单元格的Excel到HttpServletResponse
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String title, List<HeadField> heads, List<T> datas, HttpServletResponse response, boolean printFooter) {
		exportMerge(title, heads, datas, response, printFooter, true);
	}

	/**
	 * 导出带（列）合并单元格的Excel到HttpServletResponse
	 *
	 * @param fileName	文件名
	 *
	 * @param title
	 *            表格标题/文件名
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 *
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String fileName, String title, List<HeadField> heads, List<T> datas, HttpServletResponse response, boolean printFooter) {
		exportMerge(fileName, title, heads, datas, response, printFooter, true);
	}

	/**
	 * 导出Excel到OutputStream
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param out
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * @param hasTitle
	 *            是否创建大标题
	 *            
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String title, List<HeadField> heads, List<T> datas, OutputStream out, boolean printFooter, boolean hasTitle) {
		// 输出流或表头为空
		if (out == null || CollectionUtils.isEmpty(heads)) {
			throw ParamRuntimeException.NULL;
		}

		// 创建WorkBook(工作薄)
		Workbook wb = CollectionUtils.isNotEmpty(datas) && datas.size() > EXCEL_LEVEL ? new SXSSFWorkbook() : new XSSFWorkbook();
		// 创建Sheet(工作表)，使用默认名称(Sheet1 ...)
		Sheet sheet = wb.createSheet();
		// 列数
		int colCount = heads.size();
		// 创建标题：0行
		if (hasTitle) {
			hasTitle = StringUtils.isNotBlank(title);
			if (hasTitle) {
				createTitle(wb, sheet, title, colCount);
			}
		}
		// 创建表头：1行
		createHeads(hasTitle, wb, sheet, heads, colCount);
		// 创建数据
		if (CollectionUtils.isNotEmpty(datas)) {
			createDatas(hasTitle, wb, sheet, heads, datas, colCount);
			// 设置页脚
			if (printFooter) {
				Footer footer = sheet.getFooter();
				footer.setLeft(HSSFFooter.date());
				footer.setCenter(title);
				footer.setRight("第 " + HSSFFooter.page() + " 页 , 共  " + HSSFFooter.numPages() + " 页");
			}
		}

		// 输出表格
		try {
			wb.write(out);
		} catch (OpenXML4JRuntimeException e){
			// 降级【(ZipPackage 543:) Fail to save: an error occurs while saving the package : The part /docProps/core.xml failed to be saved in the stream with marshaller 】
			if (e.getMessage().startsWith("Fail to save")) {
				throw new BizRuntimeException(EXP + "网络异常，或数据过大", e);
			}else{
				throw new IoRuntimeException(EXP + e.getMessage(), e);
			}
		} catch (IOException e) {
			throw new IoRuntimeException(EXP + e.getMessage(), e);
		} finally {
			CloseUtil.flush(out);
			CloseUtil.close(out);
			CloseUtil.close(wb);
		}
	}

	/**
	 * 导出Excel到HttpServletResponse
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * @param hasTitle
	 *            是否创建大标题
	 *            
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String title, List<HeadField> heads, List<T> datas, HttpServletResponse response, boolean printFooter, boolean hasTitle) {
		export(null, title, heads, datas, response, printFooter, hasTitle);
	}

	/**
	 * 导出Excel到HttpServletResponse
	 *
	 * @param fileName
	 *  			文件名
	 * @param title
	 *            表格标题/文件名
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表
	 * @param response
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * @param hasTitle
	 *            是否创建大标题
	 *
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void export(String fileName, String title, List<HeadField> heads, List<T> datas, HttpServletResponse response, boolean printFooter, boolean hasTitle) {
		export(title, heads, datas, ServletUtil.getDownloadOut(response, (StringUtils.isBlank(title) ? (StringUtils.isBlank(fileName) ? LocalDateUtil.toDateStr() : fileName) : title) + EXCEL_TAG), printFooter, hasTitle);
	}

	/**
	 * 导出带合并单元格的Excel到OutputStream
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表：元素拥有List类型的属性，非List类型的属性会合并单元格
	 * @param out
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * @param hasTitle
	 *            是否创建大标题
	 *            
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String title, List<HeadField> heads, List<T> datas, OutputStream out, boolean printFooter, boolean hasTitle) {
		// 输出流或表头为空
		if (out == null || CollectionUtils.isEmpty(heads)) {
			throw ParamRuntimeException.NULL;
		}

		// 创建WorkBook(工作薄)
		Workbook wb = CollectionUtils.isNotEmpty(datas) && datas.size() > EXCEL_LEVEL ? new SXSSFWorkbook() : new XSSFWorkbook();
		// 创建Sheet(工作表)，使用默认名称(Sheet1 ...)
		Sheet sheet = wb.createSheet();
		// 列数
		int colCount = heads.size();
		// 创建标题：0行
		if (hasTitle) {
			hasTitle = StringUtils.isNotBlank(title);
			if (hasTitle) {
				createTitle(wb, sheet, title, colCount);
			}
		}
		// 创建表头：1行
		createHeads(hasTitle, wb, sheet, heads, colCount);
		// 创建数据
		if (CollectionUtils.isNotEmpty(datas)) {
			createMergeDatas(hasTitle, wb, sheet, heads, datas, colCount);
			// 设置页脚
			if (printFooter) {
				Footer footer = sheet.getFooter();
				footer.setLeft(HSSFFooter.date());
				footer.setCenter(title);
				footer.setRight("第 " + HSSFFooter.page() + " 页 , 共  " + HSSFFooter.numPages() + " 页");
			}
		}

		// 输出表格
		try {
			wb.write(out);
		} catch (IOException e) {
			throw new IoRuntimeException(EXP + e.getMessage(), e);
		} finally {
			CloseUtil.flush(out);
			CloseUtil.close(out);
			CloseUtil.close(wb);
		}
	}

	/**
	 * 导出带（列）合并单元格的Excel到HttpServletResponse
	 * 
	 * @param title
	 *            表格标题/文件名，非空时创建
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表：元素拥有List类型的属性，非List类型的属性会合并单元格
	 * @param response
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * @param hasTitle
	 *            是否创建大标题
	 *            
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String title, List<HeadField> heads, List<T> datas, HttpServletResponse response, boolean printFooter, boolean hasTitle) {
		exportMerge(null, title, heads, datas, response, printFooter, hasTitle);
	}

	/**
	 * 导出带（列）合并单元格的Excel到HttpServletResponse
	 *
	 * @param fileName
	 *            文件名，不包含.xlsx
	 * @param title
	 *            表格标题/文件名
	 * @param heads
	 *            表头
	 * @param datas
	 *            列表：元素拥有List类型的属性，非List类型的属性会合并单元格
	 * @param response
	 * 			  输出流：*.xlsx
	 * @param printFooter
	 *            是否打印页脚{左：日期，中：标题，右：页号}
	 * @param hasTitle
	 *            是否创建大标题
	 *
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static <T> void exportMerge(String fileName, String title, List<HeadField> heads, List<T> datas, HttpServletResponse response, boolean printFooter, boolean hasTitle) {
		exportMerge(title, heads, datas, ServletUtil.getDownloadOut(response, (StringUtils.isBlank(fileName) ? (StringUtils.isBlank(title) ? LocalDateUtil.toDateStr() : title) : fileName) + EXCEL_TAG), printFooter, hasTitle);
	}

	/**
	 * 读取Excel，封装为二维List<行:<单元格>>
	 * 
	 * @param in 输入流：*.xls或*.xlsx
	 * 
	 * @return List<行:<单元格>>：统一读取数据为字符串，调用者可以先判断字符串是否为空，然后再进行类型转换
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static List<List<String>> read(InputStream in) {
		return read(in, null, null, null, null, null);
	}

	/**
	 * 读取Excel，封装为二维List<行:<单元格>>
	 * 
	 * @param file 输入流：*.xls或*.xlsx
	 * 
	 * @return List<行:<单元格>>：统一读取数据为字符串，调用者可以先判断字符串是否为空，然后再进行类型转换
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static List<List<String>> read(MultipartFile file) {
		return read(file, null, null, null, null, null);
	}

	/**
	 * 读取Excel，封装为二维List<行:<单元格>>
	 * 
	 * @param in	输入流：*.xls或*.xlsx
	 * @param startRow
	 *            起始行：默认为0，如果实际存在的起始行号大于起始行，以实际起始行号为准
	 * 
	 * @return List<行:<单元格>>：统一读取数据为字符串，调用者可以先判断字符串是否为空，然后再进行类型转换
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static List<List<String>> read(InputStream in, Integer startRow) {
		return read(in, startRow, null, null, null, null);
	}

	/**
	 * 读取Excel，封装为二维List<行:<单元格>>
	 * 
	 * @param file
	 *            输入流：*.xls或*.xlsx
	 * @param startRow
	 *            起始行：默认为0，如果实际存在的起始行号大于起始行，以实际起始行号为准
	 * 
	 * @return List<行:<单元格>>：统一读取数据为字符串，调用者可以先判断字符串是否为空，然后再进行类型转换
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static List<List<String>> read(MultipartFile file, Integer startRow) {
		return read(file, startRow, null, null, null, null);
	}
	
	/**
	 * 读取Excel，封装为二维List<行:<单元格>>
	 * 
	 * @param file
	 *            输入流：*.xls或*.xlsx
	 * @param startRow
	 *            起始行：默认为0
	 * @param endRow
	 *            结束行：默认读取到末尾，超量以实际结束行号为准
	 * @param startCell
	 *            起始单元格，默认0
	 * @param endCell
	 *            结束单元格，默认读取到末尾，超量以实际结束列为准
	 * @param sheetIndex
	 *            工作表的索引，默认为0
	 * 
	 * @return List<行:<单元格>>：统一读取数据为字符串，调用者可以先判断字符串是否为空，然后再进行类型转换
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static List<List<String>> read(MultipartFile file, Integer startRow, Integer endRow, Integer startCell, Integer endCell, Integer sheetIndex) {
		// 数据校验
		if (file == null) {
			throw ParamRuntimeException.NULL;
		}
		try {
			return read(file.getInputStream(), startRow, endRow, startCell, endCell, sheetIndex);
		} catch (IOException e) {
			throw new IoRuntimeException("读取Excel失败：" + e.getMessage(), e);
		}
	}

	/**
	 * 读取Excel，封装为二维List<行:<单元格>>
	 * 
	 * @param in	输入流：*.xls或*.xlsx
	 * @param startRow
	 *            起始行：默认为0
	 * @param endRow
	 *            结束行：默认读取到末尾，超量以实际结束行号为准
	 * @param startCell
	 *            起始单元格，默认0
	 * @param endCell
	 *            结束单元格，默认读取到末尾，超量以实际结束列为准
	 * @param sheetIndex
	 *            工作表的索引，默认为0
	 * 
	 * @return List<行:<单元格>>：统一读取数据为字符串，调用者可以先判断字符串是否为空，然后再进行类型转换
	 * 
	 * @author 延晓磊
	 *
	 * @since 2016年8月19日
	 */
	public static List<List<String>> read(InputStream in, Integer startRow, Integer endRow, Integer startCell, Integer endCell, Integer sheetIndex) {
		// 数据校验
		if (in == null) {
			throw ParamRuntimeException.NULL;
		}
		Workbook wb = null;
		try {
			// 读取Excel文件，获取工作薄（自适应文件类型来创建HSSFWorkbook/XSSFWorkbook的实例）
			wb = WorkbookFactory.create(in);
			// 获取首个工作表
			Sheet sheet = wb.getSheetAt(IntegerUtil.toEmpty(sheetIndex));
			if (sheet == null) {
				throw new ParamRuntimeException("读取Excel失败：工作表不存在");
			}
			// 求行索引：first = 索引，last = 索引【sheet.getLastRowNum() = 末行索引】
			startRow = startRow == null ? 0 : startRow;
			endRow = endRow == null ? sheet.getLastRowNum() : Integer.min(endRow, sheet.getLastRowNum());
			// 求列索引：first = 索引，last = 索引+1【row.getLastCellNum() = 本行列数，不受合并单元格和空单元格影响】
			Row row;
			startCell = startCell == null ? 0 : startCell;
			int maxCell = 1;
			for (int i = startRow; i <= endRow; i++) {
				row = sheet.getRow(i);
				if (row != null && row.getLastCellNum() > maxCell) {
					maxCell = row.getLastCellNum();
				}
			}
			endCell = endCell == null ? maxCell : Integer.min(maxCell, endCell + 1);
			// 遍历
			List<List<String>> rows = new ArrayList<>(endRow - startRow + 1);
			List<String> cells;
			for (int i = startRow; i <= endRow; i++) {
				cells = new ArrayList<>(endCell);
				// 根据索引获取行：TreeMap，越界/为空时返回null
				row = sheet.getRow(i);
				if (row == null || row.getLastCellNum() < 1) {
					// 空行
					for (int j = startCell; j < endCell; j++) {
						cells.add(StringConstant.EMPTY);
					}
				}else{
					// 根据索引获取单元格：MissingCellPolicy.RETURN_NULL_AND_BLANK，越界/为空时返回null
					for (int j = startCell; j < endCell; j++) {
						cells.add(ExcelCellUtil.read(row.getCell(j)));
					}
				}
				rows.add(cells);
			}
			return rows;
		} catch (IOException | OfficeXmlFileException e) {
			if (StringUtils.isNotBlank(e.getMessage()) && e.getMessage().contains("Your InputStream was neither")) {
				// 文件有问题：Your InputStream was neither an OLE2 stream, nor an OOXML stream
				throw new ParamRuntimeException(LanguageUtil.match("Excel格式错误", "Excel format error"), e);
			}else{
				// 处理有问题
				throw new ParamRuntimeException(LanguageUtil.match("读取Excel失败", "Read Excel failed"), e);
			}
		} finally {
			// 释放资源
			CloseUtil.close(in);
			CloseUtil.close(wb);
		}
	}

	/**
	 * 添加图片
	 *
	 * @param wb
	 * @param pictureData		图片数据
	 * @param pictureType		图片类型：Workbook.PICTURE_TYPE_PNG
	 * @param pictureConsumer	图片处理：pic.getClientAnchor().set*, pic.resize()
	 *
	 * @author 延晓磊
	 * @since 2023年09月28日
	 */
	public static void addPicture(Workbook wb, byte[] pictureData, int pictureType, Consumer<Picture> pictureConsumer){
		// 创建图片
		int pictureIndex = wb.addPicture(pictureData, pictureType);
		// 插入图片
		pictureConsumer.accept(wb.getSheetAt(0).createDrawingPatriarch().createPicture(wb.getCreationHelper().createClientAnchor(), pictureIndex));
	}

	/**
	 * 添加图片
	 *
	 * @param wb
	 * @param picture			图片地址：绝对路径（以/或?:开头） || ClassPath地址（前缀非/） || URL地址（前缀http）
	 * @param pictureConsumer	图片处理：pic.getClientAnchor().set*, pic.resize()
	 *
	 * @author 延晓磊
	 * @since 2023年09月28日
	 */
	public static void addPicture(Workbook wb, String picture, Consumer<Picture> pictureConsumer){
		// 图片数据
		byte[] pictureData = IoUtil.readToBytes(IoUtil.getInputStreamByPath(picture));
		// 图片类型
		int pictureType = picture.endsWith("png") ? Workbook.PICTURE_TYPE_PNG : Workbook.PICTURE_TYPE_JPEG;
		// 添加图片
		addPicture(wb, pictureData, pictureType, pictureConsumer);
	}

	/**
	 * 创建标题
	 * 
	 * @param wb
	 *            工作薄
	 * @param sheet
	 *            工作表
	 * @param title
	 *            标题
	 * @param colCount
	 *            表格的列数
	 */
	private static void createTitle(Workbook wb, Sheet sheet, String title, int colCount) {
		// 创建第一行
		Row row = sheet.createRow(0);
		// 设置行高
		row.setHeight((short) 600);
		// 创建第一行的第一个单元格
		Cell cell = row.createCell(0);
		// 合并单元格
		ExcelCellUtil.merge(sheet, 0, 0, 0, colCount - 1);
		// 设置单元格的风格
		cell.setCellStyle(titleStyle(wb));
		// 设置单元格的内容
		cell.setCellValue(title);
	}

	/**
	 * 创建表头
	 * 
	 * @param hasTitle
	 *            是否有标题
	 * @param wb
	 *            工作薄
	 * @param sheet
	 *            工作表
	 * @param heads
	 *            表头
	 * @param colCount
	 *            表格的列数
	 */
	private static void createHeads(Boolean hasTitle, Workbook wb, Sheet sheet, List<HeadField> heads, int colCount) {
		// 创建第二行
		Row row = sheet.createRow(hasTitle ? 1 : 0);
		// 设置行高
		row.setHeight((short) 420);
		// 遍历表头
		Cell cell;
		CellStyle headStyle = headStyle(wb);
		for (int i = 0; i < colCount; i++) {
			// 创建单元格
			cell = row.createCell(i);
			// 设置风格
			cell.setCellStyle(headStyle);
			// 设置单元格的内容
			cell.setCellValue(heads.get(i).getTitle());
			// 设置列宽：每个字符256
			sheet.setColumnWidth(i, heads.get(i).getWidth() * 256);
		}
	}

	/**
	 * 创建数据
	 * 
	 * @param hasTitle
	 *            是否有标题行
	 * @param wb
	 *            工作薄
	 * @param sheet
	 *            工作表
	 * @param heads
	 *            表头集
	 * @param datas
	 *            数据集
	 * @param colCount
	 *            总列数
	 */
	private static <T> void createDatas(Boolean hasTitle, Workbook wb, Sheet sheet, List<HeadField> heads, List<T> datas, int colCount) {
		Object obj;
		Object value;
		HeadField head;
		Row row;
		Cell cell;
		// 数据单元格风格
		Map<String, CellStyle> csMap = new HashMap<>(colCount);
		// 遍历数据
		int startRow = hasTitle ? 2 : 1;
		for (int i = 0; i < datas.size(); i++) {
			// 数据
			obj = datas.get(i);
			// 数据非空处理
			if (obj == null) {
				continue;
			}
			// 创建行
			row = sheet.createRow(i + startRow);
			// 遍历表头
			for (int j = 0; j < colCount; j++) {
				// 表头
				head = heads.get(j);
				if (StringUtils.isBlank(head.getField())) {
					throw new ParamRuntimeException(EXP_FIELD);
				}
				// 创建单元格
				cell = row.createCell(j);
				// 反射获取单元格的内容
				if (FieldName.INDEX.equals(head.getField())) {
					value = String.valueOf(i + 1);
				}else{
					try {
						value = Ognl.getValue(head.getField(), obj);
					} catch (Exception e) {
						throw new ReflexRuntimeException(EXP_DATA + e.getMessage(), e);
					}
				}
				// 设置单元格的样式和内容
				setCell(cell, csMap, wb, value, head);
			}
		}
	}

	/**
	 * 创建带（列）合并单元格的数据
	 * 
	 * @param hasTitle
	 *            是否有标题行
	 * @param wb
	 *            工作薄
	 * @param sheet
	 *            工作表
	 * @param heads
	 *            表头集
	 * @param datas
	 *            数据集，元素有List类型的属性
	 * @param colCount
	 *            总列数
	 */
	private static <T> void createMergeDatas(Boolean hasTitle, Workbook wb, Sheet sheet, List<HeadField> heads, List<T> datas, int colCount) {
		Object value;
		HeadField head;
		boolean isList;
		Row row;
		Cell cell;
		CellStyle cs = null;
		int maxListSize;
		int tmpListSize;
		List<?> tmpList;
		// 数据单元格风格
		Map<String, CellStyle> csMap = new HashMap<>(colCount);
		// 遍历数据
		int startRow = hasTitle ? 2 : 1;
		int i = 0;
		int i2;
		int i3;
		for (Object obj : datas) {
			// 数据非空处理
			if (obj == null) {
				continue;
			}

			// 遍历表头
			maxListSize = 1;
			for (int j = 0; j < colCount; j++) {
				// 表头
				head = heads.get(j);
				isList = head.isList();
				if (StringUtils.isBlank(head.getField())) {
					throw new ParamRuntimeException(EXP_FIELD);
				}

				// 反射获取元素的内容
				try {
					value = Ognl.getValue(head.getField(), obj);
				} catch (Exception e) {
					throw new ReflexRuntimeException(EXP_DATA + e.getMessage(), e);
				}

				// 获取最大list长度
				if (isList) {
					if (value == null) {
						continue;
					}
					tmpListSize = ((List<?>) value).size();
					maxListSize = Math.max(tmpListSize, maxListSize);
				}
			}

			// 创建行
			i2 = i;
			for (int k = 0; k < maxListSize; k++) {
				sheet.createRow(i + startRow);
				i++;
			}

			// 遍历表头
			for (int j = 0; j < colCount; j++) {
				// 表头
				head = heads.get(j);
				isList = head.isList();
				if (StringUtils.isBlank(head.getField())) {
					throw new ParamRuntimeException(EXP_FIELD);
				}

				// 反射获取元素的内容
				try {
					value = Ognl.getValue(head.getField(), obj);
				} catch (Exception e) {
					throw new ReflexRuntimeException(EXP_DATA + e.getMessage(), e);
				}

				// 非list
				if (!isList) {
					// 获取行
					row = sheet.getRow(i2 + startRow);
					// 创建单元格
					cell = row.createCell(j);
					// 设置单元格的样式和内容
					cs = setCell(cell, csMap, wb, value, head);

					// 其它单元格
					for (int l = 1; l < maxListSize; l++) {
						// 获取行
						row = sheet.getRow(i2 + startRow + l);
						// 创建单元格
						cell = row.createCell(j);
						// 设置单元格格式
						cell.setCellStyle(cs);
					}

					// 合并单元格
					if (maxListSize > 1) {
						ExcelCellUtil.merge(sheet, i2 + startRow, i2 + startRow + maxListSize - 1, j, j);
					}
				} else {
					// list
					tmpList = value == null ? null : (List<?>) value;
					if (tmpList == null || tmpList.isEmpty()) {
						// 空单元格
						for (int l = 0; l < maxListSize; l++) {
							// 获取行
							row = sheet.getRow(i2 + startRow + l);
							// 创建单元格
							cell = row.createCell(j);
							// 设置单元格的样式和内容
							cs = setCell(cell, csMap, wb, null, head);
						}
					} else {
						// 数据单元格
						i3 = i2;
						for (Object data : tmpList) {
							// 获取行
							row = sheet.getRow(i3 + startRow);
							// 创建单元格
							cell = row.createCell(j);
							// 设置单元格的样式和内容
							cs = setCell(cell, csMap, wb, data, head);
							i3++;
						}

						// 其它单元格
						for (int l = tmpList.size(); l < maxListSize; l++) {
							// 获取行
							row = sheet.getRow(i2 + startRow + l);
							// 创建单元格
							cell = row.createCell(j);
							// 设置单元格格式
							cell.setCellStyle(cs);
						}
					}
				}
			}
		}
	}

	/**
	 * 设置单元格
	 * 
	 * @param cell
	 *            单元格
	 * @param csMap
	 *            单元格样式集
	 * @param wb
	 *            工作薄
	 * @param value
	 *            值
	 * @param head
	 *            字段头信息
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年4月9日
	 */
	private static CellStyle setCell(Cell cell, Map<String, CellStyle> csMap, Workbook wb, Object value, HeadField head) {
		// 设置单元格式样
		CellStyle cStyle = csMap.get(head.getField());
		if (cStyle == null) {
			cStyle = dataStyle(wb, head);
			csMap.put(head.getField(), cStyle);
		}
		cell.setCellStyle(cStyle);
		// 设置单元格值
		if (head.isFx()) {
			// 函数
			cell.setCellFormula(StringUtil.toString(value));
		}else{
			// 数据
			String val = dataFormat(value, head.getFormat());
			if (FieldFormat.TEXT.getFormat().equals(head.getFormat().getFormat())) {
				// 文本：字符串，日期格式化，未设置Format的数字，其它
				cell.setCellValue(val);
			} else {
				// 数值：设置了Format > 转换为Double > 由POI进行格式化
				cell.setCellValue(DoubleUtil.valueOfZero(val));
			}
		}
		return cStyle;
	}

	/**
	 * 标题样式
	 * 
	 * @param wb
	 *            工作薄
	 * @return
	 */
	private static CellStyle titleStyle(Workbook wb) {
		// 创建表格样式
		CellStyle cs = wb.createCellStyle();
		// 居中
		ExcelStyleUtil.align(cs, null, null);
		// 字体
		ExcelStyleUtil.font(wb, cs, null, (short)14, true);
		// 换行
		ExcelStyleUtil.wrapText(cs);
		return cs;
	}

	/**
	 * 表头样式
	 * 
	 * @param wb
	 *            工作薄
	 * @return
	 */
	private static CellStyle headStyle(Workbook wb) {
		// 创建表格样式
		CellStyle cs = wb.createCellStyle();
		// 居中
		ExcelStyleUtil.align(cs, null, null);
		// 背景
		ExcelStyleUtil.background(cs, null);
		// 边框
		ExcelStyleUtil.border(cs);
		// 字体
		ExcelStyleUtil.font(wb, cs, null, (short)12, true);
		// 换行
		ExcelStyleUtil.wrapText(cs);
		return cs;
	}

	/**
	 * 内容样式
	 * 
	 * @param wb 工作薄
	 * @param head 头信息
	 * @return
	 */
	private static CellStyle dataStyle(Workbook wb, HeadField head) {
		// 创建表格样式
		CellStyle cs = wb.createCellStyle();
		// 居中
		ExcelStyleUtil.align(cs, head.getAlign(), null);
		// 边框
		ExcelStyleUtil.border(cs);
		// 字体
		ExcelStyleUtil.font(wb, cs, head.getColor(), (short) 10, false);
		// 换行
		ExcelStyleUtil.wrapText(cs);
		// 单元格格式
		cs.setDataFormat(wb.createDataFormat().getFormat(head.getFormat().getFormat()));
		return cs;
	}

	/**
	 * 数据格式化
	 * 
	 * @param value
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2018年7月20日
	 */
	private static String dataFormat(Object value, FieldFormat format) {
		if (value == null) {
			return StringConstant.EMPTY;
		} else {
			// 类型转换及格式化
			if (value instanceof String) {
				// 字符串
				return StringUtil.toEmptyString(value);
			} else if (value instanceof Date || value instanceof LocalDateTime) {
				// 日期时间
				LocalDateTime dateTime;
				if (value instanceof Date) {
					dateTime = LocalDateUtil.toLocalDateTime((Date) value);
				} else {
					dateTime = (LocalDateTime) value;
				}
				if (format == null || format.equals(FieldFormat.TEXT)) {
					return LocalDateUtil.toStr(dateTime);
				} else {
					switch (format) {
						case DATE:
							return dateTime.format(DateConstant.DATE_FROMAT);
						case TIME:
							return dateTime.format(DateConstant.TIME_FORMAT);
						case MONTH:
							return dateTime.format(DateConstant.MONTH_FORMAT);
						case DATETIME_ZH:
							return dateTime.format(DateConstant.DATE_TIME_FORMAT_ZH);
						case DATE_ZH:
							return dateTime.format(DateConstant.DATE_FORMAT_ZH);
						case TIME_ZH:
							return dateTime.format(DateConstant.TIME_FORMAT_ZH);
						case MONTH_ZH:
							return dateTime.format(DateConstant.MONTH_FORMAT_ZH);
						default:
							return dateTime.format(DateConstant.DATE_TIME_FORMAT);
					}
				}
			} else if (value instanceof LocalDate) {
				// 日期
				LocalDate dateTime = (LocalDate) value;
				if (format == null || format.equals(FieldFormat.TEXT)) {
					return LocalDateUtil.toStr(dateTime);
				} else {
					switch (format) {
						case MONTH:
							return dateTime.format(DateConstant.MONTH_FORMAT);
						case DATE_ZH:
							return dateTime.format(DateConstant.DATE_FORMAT_ZH);
						case MONTH_ZH:
							return dateTime.format(DateConstant.MONTH_FORMAT_ZH);
						default:
							return dateTime.format(DateConstant.DATE_FROMAT);
					}
				}
			} else if (value instanceof LocalTime) {
				// 时间
				LocalTime dateTime = (LocalTime) value;
				if (format == null || format.equals(FieldFormat.TEXT)) {
					return LocalDateUtil.toStr(dateTime);
				} else {
					if (format == FieldFormat.TIME_ZH) {
						return dateTime.format(DateConstant.TIME_FORMAT_ZH);
					}
					return dateTime.format(DateConstant.TIME_FORMAT);
				}
			} else if (value instanceof Number) {
				// 数值：转换为String [ 设置了Format > 转换为Double > 由POI进行格式化 ]
				return value.toString();
			} else {
				return value.toString();
			}
		}
	}


}
